/*******************************************************************************
 * Copyright (c) 2000, 2025 IBM Corporation and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.gef.commands;

import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;
import java.util.Stack;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * An implementation of a command stack. A stack manages the executing, undoing,
 * and redoing of {@link Command Commands}. Executed commands are pushed onto a
 * a stack for undoing later. Commands which are undone are pushed onto a redo
 * stack. Whenever a new command is executed, the redo stack is flushed.
 * <P>
 * A CommandStack contains a dirty property. This property can be used to
 * determine when persisting changes is required. The stack is dirty whenever
 * the last executed or redone command is different than the command that was at
 * the top of the undo stack when {@link #markSaveLocation()} was last called.
 * Initially, the undo stack is empty, and not dirty.
 *
 * @author hudsonr
 */
public class CommandStack {

	/**
	 * Constant indicating notification after a command has been executed.
	 */
	public static final int POST_EXECUTE = 8;
	/**
	 * Constant indicating notification after a command has been redone.
	 */
	public static final int POST_REDO = 16;
	/**
	 * Constant indicating notification after a command has been undone.
	 */
	public static final int POST_UNDO = 32;

	/**
	 * Constant indicating notification after flushing the stack.
	 *
	 * @since 3.11
	 */
	public static final int POST_FLUSH = 256;

	/**
	 * Constant indicating notification after marking the save location of the
	 * stack.
	 *
	 * @since 3.11
	 */
	public static final int POST_MARK_SAVE = 512;

	/**
	 * A bit-mask indicating notification after the command stack has changed. This
	 * includes after a command has been undone, redone, or executed, as well as
	 * after it has been flushed or the save location has been marked.
	 * <P>
	 * Usage<BR/>
	 *
	 * <PRE>
	 * if ((commandStackEvent.getDetail() &amp; CommandStack.POST_MASK) != 0) {
	 * 	// Do something, like:
	 * 	stopBatchingChanges();
	 * }
	 * </PRE>
	 */
	public static final int POST_MASK = POST_EXECUTE | POST_UNDO | POST_REDO | POST_FLUSH | POST_MARK_SAVE;

	/**
	 * Constant indicating notification prior to executing a command.
	 */
	public static final int PRE_EXECUTE = 1;

	/**
	 * Constant indicating notification prior to redoing a command.
	 */
	public static final int PRE_REDO = 2;

	/**
	 * Constant indicating notification prior to undoing a command.
	 */
	public static final int PRE_UNDO = 4;

	/**
	 * Constant indicating notification prior to flushing the stack.
	 *
	 * @since 3.11
	 */
	public static final int PRE_FLUSH = 64;

	/**
	 * Constant indicating notification prior to marking the save location of the
	 * stack.
	 *
	 * @since 3.11
	 */
	public static final int PRE_MARK_SAVE = 128;

	/**
	 * A bit-mask indicating notification before the command stack is changed. This
	 * includes before a command has been undone, redone, or executed, and before
	 * the stack is being flushed or the save location is marked.
	 * <P>
	 * Usage<BR/>
	 *
	 * <PRE>
	 * if ((commandStackEvent.getDetail() &amp; CommandStack.PRE_MASK) != 0) {
	 * 	// Do something, like:
	 * 	startBatchingChanges();
	 * }
	 * </PRE>
	 *
	 * @since 3.7 Had package visibility before.
	 */
	public static final int PRE_MASK = PRE_EXECUTE | PRE_UNDO | PRE_REDO | PRE_FLUSH | PRE_MARK_SAVE;

	private final List<CommandStackEventListener> eventListeners = new CopyOnWriteArrayList<>();

	/**
	 * The list of {@link CommandStackListener}s.
	 *
	 * @deprecated This field should not be referenced, use
	 *             {@link #notifyListeners()}. This field will be removed after the
	 *             2027-03 release.
	 */
	@Deprecated(since = "3.11", forRemoval = true)
	protected List<CommandStackListener> listeners = new CopyOnWriteArrayList<>();

	private final Stack<Command> redoable = new Stack<>();

	private int saveLocation = 0;

	private final Stack<Command> undoable = new Stack<>();

	private int undoLimit = 0;

	/**
	 * Constructs a new command stack. By default, there is no undo limit, and
	 * isDirty() will return <code>false</code>.
	 */
	public CommandStack() {
	}

	/**
	 * Appends the listener to the list of command stack listeners. Multiple adds
	 * result in multiple notifications.
	 *
	 * @since 3.1
	 * @param listener the event listener
	 */
	public void addCommandStackEventListener(CommandStackEventListener listener) {
		eventListeners.add(listener);
	}

	/**
	 * Appends the listener to the list of command stack listeners. Multiple adds
	 * will result in multiple notifications.
	 *
	 * @param listener the listener
	 * @deprecated Use
	 *             {@link #addCommandStackEventListener(CommandStackEventListener)}
	 *             instead. This method will be removed after the 2027-03 release.
	 */
	@Deprecated(since = "3.11", forRemoval = true)
	public void addCommandStackListener(CommandStackListener listener) {
		listeners.add(listener);
	}

	/**
	 * @return <code>true</code> if it is appropriate to call {@link #redo()}.
	 */
	public boolean canRedo() {
		if (redoable.isEmpty()) {
			return false;
		}
		return redoable.peek().canRedo();
	}

	/**
	 * @return <code>true</code> if {@link #undo()} can be called
	 */
	public boolean canUndo() {
		if (undoable.isEmpty()) {
			return false;
		}
		return undoable.peek().canUndo();
	}

	/**
	 * This will <code>dispose()</code> all the commands in both the undo and redo
	 * stack. Both stacks will be empty afterwards.
	 */
	public void dispose() {
		flushUndo();
		flushRedo();
	}

	/**
	 * Executes the specified Command if possible. Prior to executing the command, a
	 * CommandStackEvent for {@link #PRE_EXECUTE} will be fired to event listeners.
	 * Similarly, after attempting to execute the command, an event for
	 * {@link #POST_EXECUTE} will be fired. If the execution of the command
	 * completely normally, stack listeners will receive
	 * {@link CommandStackListener#commandStackChanged(EventObject) stackChanged}
	 * notification.
	 * <P>
	 * If the command is <code>null</code> or cannot be executed, nothing happens.
	 *
	 * @param command the Command to execute
	 * @see CommandStackEventListener
	 */
	public void execute(Command command) {
		if (command == null || !command.canExecute()) {
			return;
		}
		flushRedo();
		notifyListeners(command, PRE_EXECUTE);
		try {
			command.execute();
			if (getUndoLimit() > 0) {
				while (undoable.size() >= getUndoLimit()) {
					undoable.remove(0).dispose();
					if (saveLocation > -1) {
						saveLocation--;
					}
				}
			}
			if (saveLocation > undoable.size()) {
				saveLocation = -1; // The save point was somewhere in the redo
			}
			// stack
			undoable.push(command);
			notifyListeners();
		} finally {
			notifyListeners(command, POST_EXECUTE);
		}
	}

	/**
	 * Flushes the entire stack and resets the save location to zero. This method
	 * might be called when performing "revert to saved".
	 */
	public void flush() {
		notifyListeners(null, PRE_FLUSH);
		flushRedo();
		flushUndo();
		saveLocation = 0;
		notifyListeners();
		notifyListeners(null, POST_FLUSH);
	}

	private void flushRedo() {
		while (!redoable.isEmpty()) {
			redoable.pop().dispose();
		}
	}

	private void flushUndo() {
		while (!undoable.isEmpty()) {
			undoable.pop().dispose();
		}
	}

	/**
	 * @return an array containing all commands in the order they were executed
	 */
	public Object[] getCommands() {
		List<Command> commands = new ArrayList<>(undoable);
		for (int i = redoable.size() - 1; i >= 0; i--) {
			commands.add(redoable.get(i));
		}
		return commands.toArray();
	}

	/**
	 * Peeks at the top of the <i>redo</i> stack. This is useful for describing to
	 * the User what will be redone. The returned <code>Command</code> has a label
	 * describing it.
	 *
	 * @return the top of the <i>redo</i> stack, which may be <code>null</code>
	 */
	public Command getRedoCommand() {
		return redoable.isEmpty() ? null : (Command) redoable.peek();
	}

	/**
	 * Peeks at the top of the <i>undo</i> stack. This is useful for describing to
	 * the User what will be undone. The returned <code>Command</code> has a label
	 * describing it.
	 *
	 * @return the top of the <i>undo</i> stack, which may be <code>null</code>
	 */
	public Command getUndoCommand() {
		return undoable.isEmpty() ? null : (Command) undoable.peek();
	}

	/**
	 * Returns the undo limit. The undo limit is the maximum number of atomic
	 * operations that the User can undo. <code>-1</code> is used to indicate no
	 * limit.
	 *
	 * @return the undo limit
	 */
	public int getUndoLimit() {
		return undoLimit;
	}

	/**
	 * Returns true if the stack is dirty. The stack is dirty whenever the last
	 * executed or redone command is different than the command that was at the top
	 * of the undo stack when {@link #markSaveLocation()} was last called.
	 *
	 * @return <code>true</code> if the stack is dirty
	 */
	public boolean isDirty() {
		return undoable.size() != saveLocation;
	}

	/**
	 * Marks the last executed or redone Command as the point at which the changes
	 * were saved. Calculation of {@link #isDirty()} will be based on this
	 * checkpoint.
	 */
	public void markSaveLocation() {
		notifyListeners(null, PRE_MARK_SAVE);
		saveLocation = undoable.size();
		notifyListeners();
		notifyListeners(null, POST_MARK_SAVE);
	}

	/**
	 * Sends notification to all {@link CommandStackListener}s.
	 *
	 * @deprecated Use {@link #notifyListeners(Command, int)} instead. This method
	 *             will be removed after the 2027-03 release.
	 */
	@Deprecated(since = "3.11", forRemoval = true)
	protected void notifyListeners() {
		EventObject event = new EventObject(this);
		listeners.forEach(listener -> listener.commandStackChanged(event));
	}

	/**
	 * Notifies command stack event listeners that the command stack has changed to
	 * the specified state.
	 *
	 * @param command the command
	 * @param state   the current stack state
	 * @since 3.2
	 */
	protected void notifyListeners(Command command, int state) {
		CommandStackEvent event = new CommandStackEvent(this, command, state);
		eventListeners.forEach(eventListener -> eventListener.stackChanged(event));
	}

	/**
	 * Calls redo on the Command at the top of the <i>redo</i> stack, and pushes
	 * that Command onto the <i>undo</i> stack. This method should only be called
	 * when {@link #canUndo()} returns <code>true</code>.
	 */
	public void redo() {
		// Assert.isTrue(canRedo())
		if (!canRedo()) {
			return;
		}
		Command command = redoable.pop();
		notifyListeners(command, PRE_REDO);
		try {
			command.redo();
			undoable.push(command);
			notifyListeners();
		} finally {
			notifyListeners(command, POST_REDO);
		}
	}

	/**
	 * Removes the first occurrence of the specified listener.
	 *
	 * @param listener the listener
	 */
	public void removeCommandStackEventListener(CommandStackEventListener listener) {
		eventListeners.remove(listener);
	}

	/**
	 * Removes the first occurrence of the specified listener.
	 *
	 * @param listener the listener
	 * @deprecated Use {@link CommandStackEventListener} instead. This method will
	 *             be removed after the 2027-03 release.
	 */
	@Deprecated(since = "3.11", forRemoval = true)
	public void removeCommandStackListener(CommandStackListener listener) {
		listeners.remove(listener);
	}

	/**
	 * Sets the undo limit. The undo limit is the maximum number of atomic
	 * operations that the User can undo. <code>-1</code> is used to indicate no
	 * limit.
	 *
	 * @param undoLimit the undo limit
	 */
	public void setUndoLimit(int undoLimit) {
		this.undoLimit = undoLimit;
	}

	/**
	 * Undoes the most recently executed (or redone) Command. The Command is popped
	 * from the undo stack to and pushed onto the redo stack. This method should
	 * only be called when {@link #canUndo()} returns <code>true</code>.
	 */
	public void undo() {
		if (!canUndo()) {
			return;
		}
		// Assert.isTrue(canUndo());
		Command command = undoable.pop();
		notifyListeners(command, PRE_UNDO);
		try {
			command.undo();
			redoable.push(command);
			notifyListeners();
		} finally {
			notifyListeners(command, POST_UNDO);
		}
	}

}
